package theeg.DependencyAnalysis.Core.Internal;

import LEX.Function2;
import LEX.Queryable;
import LEX.Tuple;
import theeg.DependencyAnalysis.Core.DataModel.*;
import theeg.DependencyAnalysis.Core.DependencyException;
import theeg.DependencyAnalysis.Core.Verifier;
import theeg.DependencyAnalysis.Core.VerifierUtilities;

import java.util.*;
import java.util.function.Predicate;

import static LEX.LanguageExtensions.as;

public class VerifierImplementation implements Verifier {

    private final DependencyModel _dependencyModel;

    public VerifierImplementation(DependencyModel dependencyModel) {
        _dependencyModel = dependencyModel;
    }

    public void VerifyForeignModulesByName(Iterable<String> allowedForeigners) {
        VerifyModules(allowedForeigners, true);
        VerifyModulesAreUsed(allowedForeigners);
    }

    private void VerifyModulesAreUsed(Iterable<String> modules) {
        List<String> usedModules = Queryable.From(_dependencyModel.getDependenciesPerModule().keySet()).Select(x -> x.getFullName()).toList();
        List<String> violations = Queryable.From(modules).Where(module -> !usedModules.contains(module)).toList();
        if (violations.size() == 0) return;
        VerifierUtilities.throwDependencyException(violations, "Unreferenced module: %s");
    }

    private void VerifyModules(Iterable<String> allowedModules, boolean foreigners) {
        Set<ModuleDescriptor> modules = _dependencyModel.getDependenciesPerModule().keySet();
        Map<ModuleDescriptor, Set<TypeDescriptor>> violations = FindViolatingModules(allowedModules, foreigners, modules);
        if (violations.size() == 0) return;

        StringBuilder message = new StringBuilder();
        for (ModuleDescriptor module : violations.keySet()) {
            Set<TypeDescriptor> referrers = violations.get(module);
            for (TypeDescriptor referrer : referrers) {
                message.append(String.format("Forbidden module: %s refers to %s", referrer.getFullName(),
                        module.getFullName()));
                message.append("\n");
            }
        }
        throw new DependencyException(message.toString());
    }

    private Map<ModuleDescriptor, Set<TypeDescriptor>> FindViolatingModules(Iterable<String> allowedModules, boolean foreigners, Collection<ModuleDescriptor> modules) {
        Map<ModuleDescriptor, Set<TypeDescriptor>> violations = new HashMap<>();
        Set<String> allowedSet = Queryable.From(allowedModules).toSet();
        for (ModuleDescriptor module : modules) {
            if (module.isPartOfProject() || foreigners && module.isPartOfFramework() || !foreigners && module.isPartOfFramework())
                continue;
            if (allowedSet.contains(module.getFullName())) continue;
            Set<TypeDescriptor> referrers;
            if (violations.containsKey(module)) {
                referrers = violations.get(module);
            } else {
                referrers = new HashSet<>();
                violations.put(module, referrers);
            }
            for (DependencyDescriptor dependency : _dependencyModel.getDependenciesPerModule().get(module)) {
                if (referrers.contains(dependency.getSource().getImplementingType())) continue;
                referrers.add(dependency.getSource().getImplementingType());
            }
        }
        return violations;
    }

    public void VerifyForeignModules(Iterable<Package> allowedForeigners) {
        VerifyForeignModulesByName(Queryable.From(allowedForeigners).Select(x -> x.getName()));
    }

    public void VerifyFrameworkModules(Iterable<Package> allowedFrameworkModules) {
        VerifyFrameworkModulesByName(Queryable.From(allowedFrameworkModules).Select(x -> x.getName()));
    }

    public void VerifyFrameworkModulesByName(Iterable<String> allowedFrameworkModules) {
        List<String> allowedModules = Queryable.From(allowedFrameworkModules).toList();
        VerifyModules(allowedModules, false);
    }

    public void VerifySubmodulesInModule(ModuleDescriptor module, Iterable<SubmoduleDescriptor> allowedSubmodules) {
        if (!_dependencyModel.getDependenciesPerModule().containsKey(module)) {
            throw new IllegalArgumentException(String.format("module '%s' is not part of the dependency model", module));
        }
        ModuleDescriptor storedModule = Queryable.From(_dependencyModel.getDependenciesPerModule().keySet()).FirstOrDefault(x -> x.equals(module));
        Set<SubmoduleDescriptor> requestedSubmodules = new HashSet<>();
        for (SubmoduleDescriptor submodule : allowedSubmodules) {
            requestedSubmodules.add(submodule);
        }

        List<SubmoduleDescriptor> violations = new ArrayList<>();
        for (SubmoduleDescriptor submodule : storedModule.getSubmodules()) {
            if (requestedSubmodules.contains(submodule)) {
                requestedSubmodules.remove(submodule);
                continue;
            }
            violations.add(submodule);
        }
        if (violations.size() > 0) {
            VerifierUtilities.throwDependencyException(violations, "Unapproved submodule: %s");
        }
        violations.clear();
        violations.addAll(requestedSubmodules);
        if (violations.size() > 0) {
            VerifierUtilities.throwDependencyException(violations, "Submodule does not exist: %s");
        }
    }

    public <T extends Descriptor> void VerifyAccessType(Descriptor referrer, T target, AbstractionType abstractionType) {
        if (target == null) throw new IllegalArgumentException("target");
        if (referrer == null) throw new IllegalArgumentException("referrer");
        Set<TypeDescriptor> sourceTypes = new HashSet<>();
        VerifierUtilities.selectTypes(_dependencyModel, referrer, sourceTypes);
        Map<? extends Descriptor, Set<DependencyDescriptor>> allDependencies = DependencyModelConverter.getDependencyStructureIn(_dependencyModel, target.getClass());
        if (!allDependencies.containsKey(target)) return;
        List<DependencyDescriptor> violations = Queryable.From(allDependencies.get(target)).Where(x -> abstractionType.And(x.getTarget().getImplementingType().getAbstractionType())
                && sourceTypes.contains(x.getSource().getImplementingType())).toList();
        if (violations.size() == 0) return;
        VerifierUtilities.throwDependencyException(violations, "Forbidden accessor: %s");
    }

    public boolean RefersTo(Class referrer, Class target) {
        return RefersTo(DescriptorBuilder.buildTypeDescriptor(referrer), DescriptorBuilder.buildTypeDescriptor(target));
    }

    public boolean RefersTo(Package referrer, Package target) {
        return RefersTo(DescriptorBuilder.buildModuleDescriptor(referrer), DescriptorBuilder.buildModuleDescriptor(target));
    }

    public <T extends Descriptor> boolean RefersTo(Descriptor referrer, T target) {
        if (referrer == null) throw new IllegalArgumentException("referrer");
        if (target == null) throw new IllegalArgumentException("target");
        Map<T, Set<DependencyDescriptor>> dependencyStructureIn = DependencyModelConverter.getDependencyStructureIn(_dependencyModel, (Class<T>)target.getClass());
        if (dependencyStructureIn != null) {
            return decomposeReferrers(referrer, target, dependencyStructureIn).size() > 0;
        }
        throw new IllegalArgumentException(String.format("The type %s is not supported for parameter target", target.getClass().getName()));
    }

    private static <T extends Descriptor> List<DependencyDescriptor> decomposeReferrers(Descriptor referrer, T target, Map<T, Set<DependencyDescriptor>> allDependencies) {
        Class<? extends Descriptor> referrerClass = referrer.getClass();
        Function2<DependencyDescriptor, Descriptor, Boolean> compareFunction = VerifierUtilities::compareToType;
        if (referrerClass == TypeDescriptor.class) compareFunction = VerifierUtilities::compareToType;
        else if (referrerClass == SubmoduleDescriptor.class) compareFunction = VerifierUtilities::compareToNamespace;
        else if (referrerClass == ModuleDescriptor.class) compareFunction = VerifierUtilities::compareToModule;
        else if (referrerClass == MemberDescriptor.class) compareFunction = VerifierUtilities::compareToMember;
        if (allDependencies.containsKey(target)) {
            Set<DependencyDescriptor> dependencies = allDependencies.get(target);
            return evaluateReferrers(dependencies, referrer, compareFunction);
        }
        throw new DependencyException(String.format("The target %s is not part of the dependency graph", target.getFullName()));
    }

    private static List<DependencyDescriptor> evaluateReferrers(Set<DependencyDescriptor> dependencies, final Descriptor referrer, Function2<DependencyDescriptor, Descriptor, Boolean> comparator) {
        return Queryable.From(dependencies).Where(x -> comparator.evaluate(x, referrer)).toList();
    }

    public <T extends Descriptor> void VerifyDoesNotReferTo(Descriptor referrer, T target) {
        if (referrer == null) throw new IllegalArgumentException("referrer");
        if (target == null) throw new IllegalArgumentException("target");
        Map<T, Set<DependencyDescriptor>> dependencyStructureIn = DependencyModelConverter.getDependencyStructureIn(_dependencyModel, (Class<T>)target.getClass());
        if (dependencyStructureIn != null) {
            List<DependencyDescriptor> forbiddenDependencies = decomposeReferrers(referrer, target, dependencyStructureIn);
            List<TypeDescriptor> violations = VerifierUtilities.aggregateDependenciesToTypes(forbiddenDependencies, true);
            if (violations.size() == 0) {
                return;
            }
            VerifierUtilities.throwDependencyException(forbiddenDependencies, "Forbidden accessor: %s");
        }
        throw new IllegalArgumentException(String.format("The type %s is not supported for parameter target", target.getClass().getName()));
    }

    public <T extends Descriptor> void VerifyNoOtherReferrers(Iterable<Descriptor> referrers, T target) {
        if (referrers == null) throw new IllegalArgumentException("referrers");
        if (target == null) throw new IllegalArgumentException("target");
        Set<String> allowedReferrers = new HashSet<>();
        aggregateAllowedReferrers(referrers, allowedReferrers, target.getClass());

        Map<? extends Descriptor, Set<DependencyDescriptor>> dependencyStructureIn = DependencyModelConverter.getDependencyStructureIn(_dependencyModel, target.getClass());
        List<DependencyDescriptor> dependencies = null;
        if (dependencyStructureIn.containsKey(target)) {
            dependencies = Queryable.From(dependencyStructureIn.get(target)).toList();
        }
        if (dependencies == null) return;

        List<DependencyDescriptor> violations = Queryable.From(dependencies).Where(x->!allowedReferrers.contains(x.getSource().getIdentifier())).toList();
        if (violations.size() == 0) return;

        VerifierUtilities.throwDependencyException(violations, "Forbidden accessor: %s");
    }

    private void aggregateAllowedReferrers(Iterable<Descriptor> referrers, Set<String> allowedReferrers, Class<? extends Descriptor> descriptorClass) {
        for (Descriptor referrer : referrers)
        {
            MemberDescriptor member = as(referrer, MemberDescriptor.class);
            if (member != null) {
                allowedReferrers.add(member.getIdentifier());
                continue;
            }
            TypeDescriptor type = as(referrer, TypeDescriptor.class);
            if (type != null) {
                Queryable<MemberDescriptor> selection = Queryable.From(_dependencyModel.getDependenciesPerMember().keySet())
                        .Where(x -> x.getImplementingType().getIdentifier().equals(type.getIdentifier()));
                for (MemberDescriptor m : selection) {
                    allowedReferrers.add(m.getIdentifier());
                }
                continue;
            }
            SubmoduleDescriptor namespaceDescriptorRef = as(referrer, SubmoduleDescriptor.class);
            if (namespaceDescriptorRef != null) {
                Queryable<MemberDescriptor> selection = Queryable.From(_dependencyModel.getDependenciesPerMember().keySet())
                        .Where(x -> x.getImplementingType().getSubmodule().getIdentifier().equals(namespaceDescriptorRef.getIdentifier()));
                for (MemberDescriptor m : selection) {
                    allowedReferrers.add(m.getIdentifier());
                }
                continue;
            }
            ModuleDescriptor module = as(referrer, ModuleDescriptor.class);
            if (module != null) {
                Queryable<MemberDescriptor> selection = Queryable.From(_dependencyModel.getDependenciesPerMember().keySet())
                        .Where(x -> x.getImplementingType().getSubmodule().getModule().getIdentifier().equals(module.getIdentifier()));
                for (MemberDescriptor m : selection) {
                    allowedReferrers.add(m.getIdentifier());
                }
            }
        }
    }

    public void VerifyVersatilityForProject(List<Descriptor> allowedExceptions) {
        if (allowedExceptions == null) throw new IllegalArgumentException("allowedExceptions");
        Set<TypeDescriptor> exceptionTypes = new HashSet<>();
        if (allowedExceptions.size() > 0) {
            for (Descriptor exception : allowedExceptions)
            {
                VerifierUtilities.selectTypes(_dependencyModel, exception, exceptionTypes);
            }
        }
        Queryable<TypeDescriptor> types = Queryable.From(_dependencyModel.getDependenciesPerType().keySet())
                .Where(x->x.getSubmodule().getModule().isPartOfProject() && !exceptionTypes.contains(x));

        StringBuilder errorMessage = new StringBuilder();
        for (TypeDescriptor typeDescriptor : types)
        {
            AppendVerifyVersatilityMessage(typeDescriptor, errorMessage);
        }
        if (errorMessage.length() > 0) {
            throw new DependencyException(errorMessage.toString());
        }
    }

    public void VerifyVersatility(Descriptor toVerify, List<Descriptor> allowedExceptions) {
        if (toVerify == null) throw new IllegalArgumentException("toVerify");
        if (allowedExceptions == null) throw new IllegalArgumentException("allowedExceptions");
        Set<TypeDescriptor> includedTypes = new HashSet<>();
        VerifierUtilities.selectTypes(_dependencyModel, toVerify, includedTypes);
        Set<TypeDescriptor> exceptionTypes = new HashSet<>();
        if (allowedExceptions.size() > 0) {
            for (Descriptor exception : allowedExceptions)
            {
                VerifierUtilities.selectTypes(_dependencyModel, exception, exceptionTypes);
            }
        }
        Queryable<TypeDescriptor> types = Queryable.From(_dependencyModel.getDependenciesPerType().keySet())
                .Where(x->x.getSubmodule().getModule().isPartOfProject() && !exceptionTypes.contains(x) && includedTypes.contains(x));

        StringBuilder errorMessage = new StringBuilder();
        for (TypeDescriptor typeDescriptor : types)
        {
            AppendVerifyVersatilityMessage(typeDescriptor, errorMessage);
        }
        if (errorMessage.length() > 0) {
            throw new DependencyException(errorMessage.toString());
        }
    }

    public void VerifyVersatility(TypeDescriptor type) {
        if (type.isGenerated()) return;
        StringBuilder errorMessage = new StringBuilder();
        if (!_dependencyModel.getDependenciesPerType().containsKey(type)) {
            errorMessage.append(String.format("the type %s cannot be found\n", type.getFullName()));
        } else {
            AppendVerifyVersatilityMessage(type, errorMessage);
        }
        if (errorMessage.length() > 0) {
            throw new DependencyException(errorMessage.toString());
        }
    }

    private void AppendVerifyVersatilityMessage(TypeDescriptor type, StringBuilder errorMessage) {
        if (!_dependencyModel.getDependenciesPerType().containsKey(type)) {
            errorMessage.append(String.format("the type %s cannot be found\n", type.getFullName()));
            return;
        }
        List<DependencyDescriptor> dependencies = new ArrayList<>();
        dependencies.addAll(_dependencyModel.getDependenciesPerType().get(type));
        if (type.getImplementingTypes() != null) {
            for (TypeDescriptor implementer : type.getImplementingTypes())
            {
                dependencies.addAll(_dependencyModel.getDependenciesPerType().get(implementer));
            }
        }
        if (dependencies.size() == 0) {
            errorMessage.append(String.format("the type %s is not referenced\n", type.getFullName()));
            return;
        }
        Set<TypeDescriptor> referencingTypes = new HashSet<>();
        for (DependencyDescriptor dependency : dependencies)
        {
            if (referencingTypes.contains(dependency.getSource().getImplementingType())) continue;
            referencingTypes.add(dependency.getSource().getImplementingType());
            if (referencingTypes.size() > 1) break;
        }
        if (referencingTypes.size() == 1) {
            errorMessage.append(String.format("the type %s is only referenced by {1}\n", type.getFullName(),
                    referencingTypes.iterator().next()));
        }
    }

    public void VerifyMembersAreUsed(Descriptor target, NodeVisibility visibility) {
        Set<MemberDescriptor> members = new HashSet<>();
        VerifierUtilities.selectMembers(_dependencyModel, target, members);

        Map<MemberDescriptor, Set<DependencyDescriptor>> dependenciesPerMember = _dependencyModel.getDependenciesPerMember();
        List<MemberDescriptor> violations = Queryable.From(members).Where(x->!x.isGenerated()&& !x.isOverride() && x.isDefinedByInterface() &&
                x.getMemberType() != MemberType.TypeReference &&
                (visibility == NodeVisibility.Ignore || x.getVisibility().And(visibility)) &&
                x.getSignature().equals("Void .ctor()") &&
                x.getSignature().equals( "Void .cctor()")).
                Where(member -> dependenciesPerMember.get(member).size() == 0).toList();

        if (violations.size() == 0) return;
        VerifierUtilities.throwDependencyException(violations, "Member %s is not referenced.");
    }

    public <T extends Descriptor> void VerifyThat(Function2<T, State, Boolean> assumption, Class<T> descriptorClass)
    {
        VerifyThat(assumption, x -> true, descriptorClass);
    }

    public <T extends Descriptor> void VerifyThat(Function2<T, State, Boolean> assumption, Predicate<T> whereClause, Class<T> descriptorClass)
    {
        Map<T, Set<DependencyDescriptor>> dependencyStructure = DependencyModelConverter.getDependencyStructureIn(_dependencyModel, descriptorClass);
        boolean violationFound = false;
        StringBuilder violations = new StringBuilder();
        for (T x : dependencyStructure.keySet())
        {
            if (!whereClause.test(x)) continue;
            State state = new State();
            if (assumption.evaluate(x, state)) continue;
            violationFound = true;
            violations.append(x.getFullName() + " violates assumption " + (state.getMessage() == null ? "" : state.getMessage()));
            violations.append("\n");
        }
        if (!violationFound) return;
        throw new DependencyException(violations.toString());
    }

    public <T extends Descriptor> List<Tuple<T, State>> Report(Function2<T, State, Boolean> metric, Class<T> descriptorClass)
    {
        return Report(metric, x -> true, descriptorClass);
    }

    public <T extends Descriptor> List<Tuple<T, State>> Report(Function2<T, State, Boolean> metric, Predicate<T> whereClause, Class<T> descriptorClass)
    {
        Map<T, Set<DependencyDescriptor>> dependencyStructure = DependencyModelConverter.getDependencyStructureIn(_dependencyModel, descriptorClass);
        List<Tuple<T, State>> list = new ArrayList<>();
        for (T x : dependencyStructure.keySet())
        {
            if (!whereClause.test(x)) continue;
            State state = new State();
            Boolean doReport = metric.evaluate(x, state);
            if (!doReport) continue;
            list.add(new Tuple<>(x, state));
        }
        return list;
    }
}